package com.efeichong.common.utils;

import com.efeichong.util.EntityUtils;
import com.efeichong.util.FastJpaSpringUtils;
import com.efeichong.uuid.UUIDUtils;
import com.google.common.collect.Lists;
import lombok.SneakyThrows;
import lombok.Synchronized;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.HashMap;
import java.util.Map;

/**
 * @author lxk
 * @date 2020/02/04
 * @description 基于redis实现的分布式重入锁
 */
@Slf4j
public class LockUtils {

    /**
     * 锁自动过期时长(大于0设置时间有效，小于等于0则为永不过期),过期自动释放,默认永不过期 单位：毫秒
     */
    public static final long LOCK_EXPIRE_TIME = 0;
    /**
     * 重试次数  默认不重试
     */
    public static final int RETRY_COUNT = 0;
    /**
     * 重试时间间隔 单位：毫秒
     */
    public static final int RETRY_TIME_INTERVAL = 10;
    /**
     * 释放锁脚本
     */
    private static final String UNLOCK_LUA;
    /**
     * 添加锁脚本
     */
    private static final String LOCK_LUA;
    /**
     * 保存锁的value，用于判断是否是重入
     */
    private static final ThreadLocal<Map<String, String>> THREAD_LOCAL = new ThreadLocal<>();
    private static StringRedisTemplate redisTemplate;

    /*
     * 释放/添加锁脚本，原子操作
     */
    static {
        StringBuilder luaScript = new StringBuilder();
        //释放锁脚本 参数： key
        luaScript.append("if redis.call('get',KEYS[1]) == ARGV[1] ");
        luaScript.append("then ");
        luaScript.append(" return redis.call('del',KEYS[1]); ");
        luaScript.append("else ");
        luaScript.append(" return 0;");
        luaScript.append("end ");
        UNLOCK_LUA = luaScript.toString();

        luaScript.setLength(0);

        //添加锁脚本 参数： key，过期时间
        luaScript.append("if 1 == redis.call('setnx' , KEYS[1] , ARGV[1]) ");
        luaScript.append("then  ");
        luaScript.append("redis.call('pexpire' , KEYS[1] , ARGV[2]) ");
        luaScript.append(" return 1; ");
        luaScript.append("else ");
        luaScript.append("return 0; ");
        luaScript.append("end ");
        LOCK_LUA = luaScript.toString();

        redisTemplate = FastJpaSpringUtils.getBean(StringRedisTemplate.class);
    }

    /**
     * 加锁
     *
     * @param key 锁的唯一标识
     */
    public static boolean lock(String key) {
        return lock(key, LOCK_EXPIRE_TIME);
    }

    /**
     * 加锁
     *
     * @param key            锁的唯一标识
     * @param lockExpireTime 锁自动过期时长 单位：毫秒
     * @return
     */
    public static boolean lock(String key, long lockExpireTime) {
        return lock(key, lockExpireTime, RETRY_COUNT, RETRY_TIME_INTERVAL);
    }

    /**
     * 加锁
     *
     * @param key            锁的唯一标识
     * @param lockExpireTime 锁自动过期时长 单位：毫秒
     * @param retryCount     自动重试次数
     * @return
     */
    @SneakyThrows
    @Synchronized
    public static boolean lock(String key, long lockExpireTime, int retryCount, long retryTimeInterval) {
        Map<String, String> originValue = THREAD_LOCAL.get();
        if (originValue == null) {
            originValue = new HashMap();
        }
        if (isReentrantLock(key, originValue)) {
            return true;
        }
        String value = UUIDUtils.getId();

        if (retryCount < 1) {
            if (setIfAbsent(key, value, lockExpireTime)) {
                originValue.put(key, value);
                THREAD_LOCAL.set(originValue);
                return true;
            }
        } else {
            int retryTimes = 1;
            while (true) {
                if (retryCount > 0) {
                    if (retryTimes > retryCount) {
                        return false;
                    }
                }
                if (setIfAbsent(key, value, lockExpireTime)) {
                    originValue.put(key, value);
                    THREAD_LOCAL.set(originValue);
                    return true;
                }
                Thread.sleep(retryTimeInterval);
                retryTimes++;
            }
        }
        return false;
    }

    /**
     * 释放锁
     *
     * @param key 锁的唯一标识
     */
    public static void unlock(String key) {
        Map<String, String> originValue = THREAD_LOCAL.get();
        if (EntityUtils.isEmpty(originValue) || EntityUtils.isEmpty(originValue.get(key))) {
            //没得到锁,无需释放
            return;
        }
        DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript(UNLOCK_LUA, Boolean.class);
        boolean unlock = redisTemplate.execute(defaultRedisScript, Lists.newArrayList(key), originValue.get(key));
        if (!unlock) {
            //没得到锁,无需释放
        }
        originValue.remove(key);
    }

    /**
     * 如果key存在返回false且不进行任何操作，不存在则设置值并返回true
     *
     * @param key            锁的唯一标识
     * @param value          值
     * @param lockExpireTime 锁自动过期时长 单位：毫秒
     * @return
     */
    private static boolean setIfAbsent(String key, String value, long lockExpireTime) {
        if (lockExpireTime <= 0) {
            return redisTemplate.opsForValue().setIfAbsent(key, value);
        } else {
            DefaultRedisScript<Boolean> defaultRedisScript = new DefaultRedisScript(LOCK_LUA, Boolean.class);
            return redisTemplate.execute(defaultRedisScript, Lists.newArrayList(key), value, lockExpireTime + "");
        }
    }

    /**
     * 是否为重入锁
     *
     * @param key         锁的唯一标识
     * @param originValue 线程中存在的实际值
     * @return
     */
    private static boolean isReentrantLock(String key, Map<String, String> originValue) {
        String value = redisTemplate.opsForValue().get(key);
        return EntityUtils.isNotEmpty(value) && EntityUtils.equals(value, originValue.get(key));
    }

    /**
     * 加锁
     *
     * @param retryCount 自动重试次数
     */
    public boolean lock(String key, int retryCount, long retryTimeInterval) {
        return lock(key, LOCK_EXPIRE_TIME, retryCount, retryTimeInterval);
    }
}
